iT邦幫忙

2025 iThome 鐵人賽

DAY 5
0
Modern Web

從 React 學 Next.js:不只要會用,還要真的懂系列 第 5

【Day 5】一樣不一樣? React Server Component (RSC) vs SSR

  • 分享至 

  • xImage
  •  

這幾天看了一些很常見的渲染模式,像是 CSR、SSR 等,接著讓我們開始和男主角 Next.js 有進一步地接觸,來了解和 Next.js 有關聯的一種 component 的類型,那就是 Server Component

SSR 的 React Server Component (RSC) 的 server 定義

在正式來看什麼是 Server Component 和 SSR 之前,我們先來看一下在這兩個名詞中的 server 指的是什麼 server。

在 SSR (Server Side Render) 中的 Server,指的是處理整個 HTTP 請求與回應的伺服器,通常是一個實體或雲端環境中的 Web Server。這裡的 server 可以是以 node.js 跑起來的 node server,也可以是其他技術跑起來的 server。而在 Next.js 中,預設使用 Node.js-based Server 或 Edge Runtime 來處理 SSR。

在 RSC (React Server Component) 中的 server 則是用來執行 RSC,甚至進一步將 RSC 序列化成 RSC Payload 的伺服器環境。而這裡所提到的 RSC Payload 指的是一個序列化物件,而不是一個實際的 HTML,也不會被直接插入 DOM。

實際觀察發送的 request,可以看到有類似這樣這樣的內容
http://localhost:3000/?_rsc=1k1pf

接著會回來類似以下這樣內容的 response,這就是 RSC payload
https://ithelp.ithome.com.tw/upload/images/20250906/20130914Q6R0w5glpZ.png

用一句話來說明的話就會是:

SSR 的 server 是負責的是「實際處理客戶端請求」,而 RSC 的 server 則是負責來「產出可被組裝成最終 HTML 的 React 元件資料的執行環境」。

雖然 SSR 的 Server 和 RSC 的 Server 負責的任務不同,但其實有可能是同個 Server,也有可能是不同台 server(如 Next.js 的 Node Runtime)。

Server Component 是什麼?

前面已經了解了 SSR 和 RSC 中的 Server 差異之後,我們再來仔細認識一下什麼是 Server Component。

Server Component 顧名思義是指在 Server 上執行的 React 元件,這是一種新型態的元件。與 SSR 不同,SSR 是以 「頁面」 為單位將整個頁面渲染為 HTML,而 Server Component 是以 「元件」 為單位在 Server 執行,它的執行結果不是 HTML,而是描述元件樹的 payload,接著會再由 React 在瀏覽器端與 Client Component 一起組成完整的畫面。

而 React Server Components(RSC)是由 React 官方提出的功能,用來定義如何將元件劃分為「Server 端執行」與「Client 端執行」的機制,並且透過序列化機制(RSC Payload),只將必要資訊傳給瀏覽器。Next.js 的角色則是穩定實作這個功能的框架,使用 App Router 時,預設就會使用RSC。雖然在 Next.js 中可以使用 Server Component,但是 Server Component 並非 Next.js 底下的產物,Next.js 只是實作並支援 React Server Component 的框架。

React Server Component 的執行時間點及渲染流程

RSC 可以在 build time 被預先執行,也可以在 runtime 根據請求被動態執行。在預設情況下,Next.js 會在 build time 就先執行 RSC,以達到效能最佳化,只有在特定條件或顯式設定(例如使用 export const dynamic = 'force-dynamic')時,才會改為在 runtime 執行。

這裡可以透過一個小小的實驗來驗證 RSC 被執行時間點的差異:

這裡先準備一個沒有動態資料的元件,把它放到 Page.tsx 檔案內使用。

const NormalRSC = () => {
  console.log("normal rsc");
  return <div>Normal RSC</div>;
};

export default NormalRSC;

我們在跑 next build 指令的時候,可以發現到在過程就會在 terminal 上印出我們要 console.log 的東西了。

https://ithelp.ithome.com.tw/upload/images/20250906/20130914RuUqOYdquh.png

另外再改成使用一個有使用動態資料的元件,如下:

async function getProjects() {
  const res = await fetch(`https://fakestoreapi.com/products`, {
    cache: "no-store",
  });
  const projects = await res.json();

  return projects;
}

const DynamicRSC = async () => {
  console.log("執行 dynamic rsc");
  const projects = await getProjects();
  console.log("dynamic rsc data: ", projects);

  return <div>{projects[0]?.title}</div>;
};

export default DynamicRSC;

當我們 build 的時候,會發現 terminal 上雖然有印出第一個 console.log 的文字,但並沒有印出 fetch 資料後的 console.log,所以雖然這個 component function 有被呼叫,但實際上只是 React 在分析這個元件的內容,所以只會執行到上方的部分,fetch 以下的內容就不會被執行,實際上完整的 RSC 執行還是會等到 runtime 的時候。
https://ithelp.ithome.com.tw/upload/images/20250906/20130914bUaQIKfh7n.png

在 runtime 的時候,才會在 terminal 印出 fetch 後的 console.log 內容。
https://ithelp.ithome.com.tw/upload/images/20250906/20130914VQkP0HadB7.png

只有在 next start 將頁面跑起來並且點進頁面的時候,才在 terminal 上印出文字。
https://ithelp.ithome.com.tw/upload/images/20250906/20130914InE0rCzvDr.png

以上這個小實驗也就可以看得出來 RSC 預設會在 build time 就執行,但是在有動態資料時,則會在 runtime 才完整地執行。

接著再來看一下 RSC 的渲染的流程。
當使用者首次進入畫面時(例如直接輸入網址或重新整理頁面),Server Component 會在伺服器端(Next.js 的 Web Server)上被執行,Next.js 會開始產生 RSC Payload,同時間會以它驅動伺服器端的 HTML 串流,並且把 RSC Payload 以 <script>self.__next_f.push(...)</script> 片段內嵌進 HTML 一起回傳到瀏覽器。

若是透過前端路由()切換頁面,Server Component 一樣會先在 React Server 上被執行,並且會序列化為 RSC Payload 傳回瀏覽器,再由 Client Runtime 將該 Payload 還原為 React Element Tree,並透過 Virtual DOM 更新畫面。

所以當首次進入頁面時會是:
RSC 被執行 → 同步產生 RSC Payload 及 HTML → 把 RSC 片段內嵌在 HTML → 將最終的 HTML 返回

用前端路由切換頁面時則是:
client 請求 RSC → RSC 被執行 → 序列化成 RSC Payload → React Element Tree → Virtual DOM → HTML (實際畫面)

另外,還有一個很重要的部分是 RSC 會是一個純靜態的元件,不包含 JavaScript,所以當你使用 onClick 這類的事件,或是使用 useState、useEffect 等的 hooks 都會跳錯。

例如這樣寫

const Page = () => {
  console.log("執行 Page");

  return (
    <>
      <h1>Home page</h1>
      <p>some description</p>
      <button onClick={() => console.log("按下按鈕")}>按鈕</button>
    </>
  );
};

export default Page;

會出現類似這樣的錯誤
https://ithelp.ithome.com.tw/upload/images/20250906/20130914vxzi6ef3cL.png

會需要在檔案中額外標示 'use client',讓 RSC 指定變成 Client Component,才可以正常使用事件及 hooks。

Server Component 和 SSR 的關係是?

看了 server 的定義,也看了什麼是 Server Component,我們接著再來看所以「Server Component 和 SSR 的關係是什麼?」,簡單來說的話,這個問題就跟「周潤發和周星馳是什麼關係?」(這是一個會透漏出自己年紀的梗) 一樣,答案是沒有關係,只是剛好名稱內都有一個 server。

SSR(Server-Side Rendering)是一種渲染模式,指的是在伺服器端將整個頁面轉換成 HTML,再回傳給瀏覽器顯示,並且是以頁面為單位進行處理。而 Server Component(RSC)則是一種新的 React 元件類型,它會在 React Server 上被執行,但產生出的並不是 HTML,而是序列化的 RSC Payload。SSR 和 Server Component 可以同時存在,也可以獨立使用。

雖然 SSR 和 Server Component 中都有 Server 這個字,不過實際上有很多不一樣的點,首先是前面提過的這兩者的 Server 負責進行任務內容不同,再來是 SSR 的單位是頁面,Server Component 單位則是元件。另外,SSR 是一種渲染模式,而 Server Component 則是一種元件的類型,所以其實兩者在代表的意義上就有所不同,千萬別再把 SSR 和 Server Component 當作是同一個東西囉!

有 Client Component 嗎?有的話,Client Component 指的是什麼?

前面一直在看 Server Component,那 Server 的相反 Client Component 這個東西是存在的嗎?有 Server Component,當然也就有 Client Component。

所謂的 Client Componet 指的並不是整個元件都只會在 client 端被執行,而是指邏輯和事件綁定這些和 Hydration 有關的部分只會在 client 端進行。實際上,Next.js 為了優化首屏下載的部分,還是會在 Server 上將可以先產生的靜態 HTML 的部分產生出來。

這邊也用一個實際的小範例來看看效果:

這裡準備了一個簡單的 Client Component

"use client";

import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";

const ClientComponent = ({ name }: { name: string }) => {
  console.log("執行 ClientComponent");
  const [finalName, setFinalName] = useState("");
  const router = useRouter();
  useEffect(() => {
    if (name !== finalName) {
      setFinalName(name);
      console.log("useEffect console.log");
    }
  }, [name, finalName]);

  return (
    <div>
      <h2>Client Component:{finalName}</h2>
      <button
        className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
        onClick={() => router.push("/about")}
      >
        Go
      </button>
    </div>
  );
};

export default ClientComponent;

可以先花幾秒思考一下幾個問題:

  1. next build 的時候,terminal 上會印出什麼文字?
  2. next start 後,response 回來的 html 內容是什麼?
  3. next start 後,瀏覽器的 dev tools 上還會印出什麼 console.log 的文字嗎?

直接來看實際的狀況是怎麼樣
首先一樣先跑 build,會發現 component 有在伺服器上被執行,因為我們可以觀察到有印出 '執行 ClientComponent',但是'useEffect console.log' 沒有被印出來。
https://ithelp.ithome.com.tw/upload/images/20250906/20130914KO31P6T7wB.png

接著跑 next start,進入頁面後,可以發現這時候 'useEffect console.log' 這段文字才被印出來。
https://ithelp.ithome.com.tw/upload/images/20250906/201309144vt7U5n8zJ.png

另外,雖然 response 回來的 HTML 有 Client Component 的靜態內容,但是卻少了在 useEffect 才設定進去的 finalName 變數的值。
https://ithelp.ithome.com.tw/upload/images/20250906/20130914pNIcZSNavj.png

由上面這些實際的測試結果就可以知道 Client Component 在 Server 上執行只會先做靜態內容的預渲染,邏輯資料和事件綁定等內容,則是會在瀏覽器上才會進行,也就是說只有和 JavaScript 相關的 Hydration 的部分會在 Client 端才進行。雖然說 Client Component 不是只能在 Client 端上執行,但是如果想要被完整的執行的確是要在 Client 端才辦得到,而且如果想要有互動操作,也就是綁定一些事件的話,一定要會需要使用 Client Component。

有 SSR,為什麼還需要 RSC?

雖然 SSR 和 RSC 中的「Server」負責不同的任務,但它們都屬於「在伺服器渲染頁面」的做法,而且目的都是為了改善 CSR 在瀏覽器端渲染時,可能會帶來的效能問題,也讓使用者在不依賴 JavaScript 的情況下,就能看到完整的頁面內容,以提升使用者的網頁體驗。

既然 SSR 就已經能解決效能和使用者體驗的部分,那為什麼還需要 RSC 呢?

主要原因有兩點: 「Hydration 的成本」「渲染顆粒度的差異」

SSR 雖然會在伺服器端將 HTML 頁面渲染好並在使用者發送請求後,並在使用者請求時回傳完整 HTML,以提升首次內容可見的速度。但為了讓頁面具有互動功能,仍需下載對應的 JavaScript 並進行 Hydration,這個過程可能會導致 JavaScript 載入量大、第一次可互動的時間延遲等問題。

而 RSC 則透過顆粒度較小的元件分工,讓不需要互動功能的 component 完全留在 server,以達到減少需載入之 JavaScript 的大小,也減少進行 Hydration 需要耗的效能,進而提升效能。

這裡也實作一個 RSC 的範例來觀察 RSC 的優點。

這裡先建立一個有標示 "use client" 的 Card 元件,把這個 Card 。元件放在畫面裡面。當進入使用 Card 元件的頁面,可以觀察到 Dev tools 裡面的 Sources 有出現 list 的 page.js。這是因為 Client Component 的主要特性就是要在瀏覽器執行,所以元件一定需要被打包成 client 端的 JavaScript,就算元件裡面只有靜態的文字內容。
https://ithelp.ithome.com.tw/upload/images/20250906/20130914d0F7FOWURf.png

如果 Card 元件沒有加上 "use client",這個元件就會變成一個 React Server Component。當整個頁面僅由 RSC 組成,且沒有使用到任何 Client Component 時,Next.js 就不會為這個 route 產生對應的 JavaScript chunk(例如 page.js),因此在 DevTools 的 Sources 面板中,也不會看到這個 page.js 檔案。
https://ithelp.ithome.com.tw/upload/images/20250906/201309147UeASWgzef.png

在這個情境下,Next.js 沒有產生 page.js 是因為 RSC 僅在伺服器上執行,會先產出 RSC Payload 並轉換成 HTML 傳給瀏覽器。由於這些元件沒有任何需要互動的行為,所以不需要進行 Hydration,也就不需要在瀏覽器端執行 JavaScript。

也就是說,如果一個頁面完全由 RSC 組成,並且使用 SSR,Next.js 就不會產出對應的 JavaScript bundle,也不會在瀏覽器中看到任何 page.js,這也就是 RSC 為什麼能減少 Hydration 的成本。

總結

Server Side Rendering(SSR)和 Server Component(RSC)雖然都帶有「Server」這個詞,但實際上關注的層面並不相同。SSR 是以「頁面」為單位的「渲染模式」,在伺服器端預先渲染整個 HTML,再傳回給使用者,目的是改善首次載入速度;而 RSC 則是以「元件」為單位在伺服器執行的「元件類型」,RSC 會產生可組合的 React 元素資料(RSC Payload),透過更小的渲染顆粒度,降低前端需載入的 JavaScript 數量與 Hydration 成本。

因此 SSR 和 RSC 並不是互相取代的功能,而是可以互補並存的存在,也能依照實際需求獨立使用。有了 RSC 的加持,Next.js 在效能優化上也更加分。

除了 Server Component,我們也認識到了 Client Component 這個元件類型,了解到 Client Component 並不是只會在 Client 端才被執行,還是會在 server 端就先被執行,只是在 Server 上只會先做靜態內容的預渲染,和 JavaScript 相關的邏輯資料,以及事件綁定等內容還是會在 client 端上才進行。

今天我們已經認識到 RSC 和 SSR 的差異,也進一步認識了 Client Component,明天我們會接著來仔細認識今天很常提到的 Hydration。

參考資料

官方文件 - Server Component
官方文件 - use client


上一篇
【Day 4】SSG vs ISR:從純靜態頁面到可自動更新的靜態頁面
下一篇
【Day 6】讓靜態頁面活起來的關鍵 - Hydration
系列文
從 React 學 Next.js:不只要會用,還要真的懂11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言